﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WebCaching = System.Web.Caching;
using System.Web;
using System.Web.SessionState;
using System.Globalization;
using System.Collections;
using System.Data;
using System.Threading;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.IO;
using blackhouse.Cache;

namespace blackhouse.Web.Cache
{
    public class WebCache : ICache
    {
        #region Private Static Fields

        private static Hashtable memoryCache = new Hashtable();
        private static List<RemoveItem> removeInfo = null;


        #endregion

        #region Public Properties

        public WebCaching.Cache Cache
        {
            get
            {
                return HttpContext.Current.Cache;
            }
        }

        public HttpCookieCollection Cookies
        {
            get
            {
                return HttpContext.Current.Request.Cookies;
            }
        }

        public HttpSessionState Session
        {
            get { return HttpContext.Current.Session; }
        }

        #endregion

        #region ICache Members

        public void Add(CacheKey key, object value)
        {
            this.Add(this.convert(key), value);
        }

        public bool Check(CacheKey key)
        {
            return this.Check(this.convert(key));
        }

        public T Get<T>(CacheKey key, GetCacheCallback<T> callback)
        {
            return this.Get<T>(this.convert(key), callback);
        }

        public object Remove(CacheKey key)
        {
            return this.Remove(this.convert(key));
        }

        #endregion

        #region Public Members

        public void Add(WebCacheKey key, object value)
        {
            if (key == null) return;

            CacheObject cacheObject = new CacheObject(key.Copy(true), value);
            switch (key.StorageSource)
            {
                case WebStorageSouce.Session:
                    this.addToSession(cacheObject);
                    break;
                case WebStorageSouce.Cookies:
                    this.addToCookies(cacheObject);
                    break;
                case WebStorageSouce.Cache:
                    this.addToCache(cacheObject);
                    break;
                default:
                    this.addToMemory(cacheObject);
                    break;
            }
            if (key.OnChanged != null)
                key.OnChanged.Invoke(key);
        }

        public bool Check(WebCacheKey key)
        {
            if (key == null) return false;
            this.checkForRemoving(this.convert(key));
            switch (key.StorageSource)
            {
                case WebStorageSouce.Session: return this.checkInSession(key);
                case WebStorageSouce.Cookies: return this.checkInCookies(key);
                case WebStorageSouce.Cache: return this.checkInCache(key);
                default: return this.checkInMemory(key);
            }
        }

        public T Get<T>(WebCacheKey key, GetCacheCallback<T> callback)
        {
            if (key == null) return callback.Invoke(key);
            T cacheObject = default(T);
            if (this.Check(key))
            {
                switch (key.StorageSource)
                {
                    case WebStorageSouce.Session:
                        cacheObject = this.getFromSession<T>(key);
                        break;
                    case WebStorageSouce.Cookies:
                        cacheObject = this.getFromCookies<T>(key);
                        break;
                    case WebStorageSouce.Cache:
                        cacheObject = this.getFromCache<T>(key);
                        break;
                    default:
                        cacheObject = this.getFromMemory<T>(key);
                        break;
                }
            }
            if (cacheObject == null || cacheObject.Equals(default(T)))
            {
                cacheObject = callback.Invoke(key);
                if (cacheObject != null)
                    this.Add(key, cacheObject);
            }
            return cacheObject;
        }

        public object Remove(WebCacheKey key)
        {
            if (key == null) return null;

            object removeObject;
            switch (key.StorageSource)
            {
                case WebStorageSouce.Session: removeObject = this.removeFromSession(key);
                    break;
                case WebStorageSouce.Cookies: removeObject = this.removeFromCookies(key);
                    break;
                case WebStorageSouce.Cache: removeObject = this.removeFromCache(key);
                    break;
                default: removeObject = this.removeFromMemory(key);
                    break;
            }
            this.Add(key, null);
            return removeObject;
        }

        #endregion

        #region Private Members

        private void addToCache(CacheObject objectToCaching)
        {
            string uniqueKey = objectToCaching.Key.ToString();
            bool eq = this.Cache.Remove(uniqueKey) == objectToCaching.Value;
            if (objectToCaching.Key.OnChanged != null && eq)
                objectToCaching.Key.OnChanged(objectToCaching.Key);
            if (objectToCaching.Value != null)
                this.Cache.Add(uniqueKey, objectToCaching, null, WebCaching.Cache.NoAbsoluteExpiration, objectToCaching.Key.TimeExpiration, System.Web.Caching.CacheItemPriority.Default, null);
        }

        private void addToCookies(CacheObject objectToCaching)
        {
            string uniqueKey = objectToCaching.Key.ToString();
            HttpCookie cookie = HttpContext.Current.Request.Cookies[uniqueKey];
            if (cookie == null)
                cookie = new HttpCookie(uniqueKey);
            else if (objectToCaching.Key.OnChanged != null && checkInCookies(objectToCaching.Key) && cookie.Value != objectToCaching.Value.ToString())
                objectToCaching.Key.OnChanged(objectToCaching.Key);

            if (objectToCaching.Value != null)
            {
                cookie.Value = HttpUtility.HtmlEncode(objectToCaching.Value.ToString());
                if (objectToCaching.Key.TimeExpiration != TimeSpan.Zero)
                    cookie.Expires = objectToCaching.Key.SetTime.Add(objectToCaching.Key.TimeExpiration);
            }
            else
            {
                cookie.HttpOnly = true;
                cookie.Value = "";
                cookie.Expires = DateTime.Now;
            }
            if (HttpContext.Current.Response.Cookies.AllKeys.Contains(uniqueKey))
            {
                HttpContext.Current.Response.Cookies[uniqueKey].HttpOnly = true;
                HttpContext.Current.Response.Cookies[uniqueKey].Value = cookie.Value;
                if (objectToCaching.Key.TimeExpiration != TimeSpan.Zero)
                    HttpContext.Current.Response.Cookies[uniqueKey].Expires = cookie.Expires;
            }
            else
            {
                HttpContext.Current.Response.Cookies.Add(cookie);
            }
        }

        private void addToMemory(CacheObject objectToCaching)
        {
            if (memoryCache.ContainsKey(objectToCaching.Key.Key))
            {
                Hashtable innerTable = memoryCache[objectToCaching.Key.Key] as Hashtable;
                if (innerTable.ContainsKey(objectToCaching.Key.Identifier))
                {
                    if (objectToCaching.Key.OnChanged != null && (innerTable[objectToCaching.Key.Identifier] as CacheObject).Value != objectToCaching.Value)
                    {
                        objectToCaching.Key.OnChanged(objectToCaching.Key);
                    }
                    innerTable.Remove(objectToCaching.Key.Identifier);
                }
            }
            if (objectToCaching.Value != null)
            {
                Hashtable innerTable;
                if (!memoryCache.ContainsKey(objectToCaching.Key.Key))
                {
                    innerTable = new Hashtable();
                    memoryCache.Add(objectToCaching.Key.Key, innerTable);
                }
                else
                    innerTable = memoryCache[objectToCaching.Key.Key] as Hashtable;
                innerTable.Add(objectToCaching.Key.Identifier, objectToCaching);
            }
        }

        private void addToSession(CacheObject objectToCaching)
        {
            string uniqueKey = objectToCaching.Key.ToString();
            if (objectToCaching.Key.OnChanged != null && this.Session[uniqueKey] != objectToCaching.Value)
                objectToCaching.Key.OnChanged(objectToCaching.Key);
            this.Session.Remove(uniqueKey);
            if (objectToCaching.Value != null)
                this.Session.Add(uniqueKey, objectToCaching);
        }

        private void checkForRemoving(WebCacheKey key)
        {
            //return;
            if (removeInfo == null)
                openRemoveInfo();
            var q = from ri in removeInfo
                    where ri.RemoveFrom == key.StorageSource
                    && ri.RemoveKey == key.Key
                    && ri.RemoveSetTime > key.SetTime
                    select ri;
            if (q.Count() > 0)
                this.Add(key, null);
        }

        private bool checkInCache(CacheKey key)
        {
            string uniqueKey = key.ToString();
            CacheObject cacheObject = this.Cache[uniqueKey] as CacheObject;
            return cacheObject != null && !cacheObject.Key.Expired();
        }

        private bool checkInCookies(CacheKey key)
        {
            string uniqueKey = key.ToString();
            HttpCookie cookie = HttpContext.Current.Request.Cookies[uniqueKey];
            return cookie != null;
        }

        private bool checkInMemory(CacheKey key)
        {
            if (memoryCache.ContainsKey(key.Key))
            {
                Hashtable innerTable = memoryCache[key.Key] as Hashtable;
                CacheObject cacheObject = innerTable[key.Identifier] as CacheObject;
                return cacheObject != null && !cacheObject.Key.Expired();
            }
            else return false;
        }

        private bool checkInSession(CacheKey key)
        {
            string uniqueKey = key.ToString();
            CacheObject cacheObject = this.Session[uniqueKey] as CacheObject;
            return cacheObject != null && !cacheObject.Key.Expired();
        }

        private WebCacheKey convert(CacheKey key)
        {
            if (key is WebCacheKey)
                return key as WebCacheKey;
            else
                return new WebCacheKey(key.Key, key.Identifier, key.SetTime, key.TimeExpiration, WebStorageSouce.Memory);
        }

        private T convertFromString<T>(string value)
        {
            try
            {
                if (typeof(T).IsPrimitive)
                    return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
                else
                    return (T)(value as object);
            }
            catch
            {
                return default(T);
            }
        }

        private T getFromCache<T>(CacheKey key)
        {
            return (T)((this.Cache[key.ToString()] is CacheObject) ? (this.Cache[key.ToString()] as CacheObject).Value : null);
        }

        private T getFromCookies<T>(CacheKey key)
        {
            HttpCookie cookie = HttpContext.Current.Request.Cookies[key.ToString()];
            if (cookie != null)
                return this.convertFromString<T>(cookie.Value);
            else return default(T);
        }

        private T getFromMemory<T>(CacheKey key)
        {
            CacheObject cache = null;
            cache = (memoryCache[key.Key] as Hashtable)[key.Identifier] as CacheObject;
            if (cache != null)
                return (T)cache.Value;
            else return default(T);
        }

        private T getFromSession<T>(CacheKey key)
        {
            return (T)((this.Session[key.ToString()] is CacheObject) ? (this.Session[key.ToString()] as CacheObject).Value : null);
        }

        private void openRemoveInfo()
        {
            using (Mutex fileLock = new Mutex(true, "readInfo"))
            {
                fileLock.WaitOne();
                string filePath = HttpContext.Current.Server.MapPath("readInfo.bin");
                if (File.Exists(filePath))
                {
                    using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                    {
                        IFormatter formatter = new BinaryFormatter();
                        List<RemoveItem> data = formatter.Deserialize(file) as List<RemoveItem>;
                        removeInfo = (data != null) ? data : new List<RemoveItem>();
                        file.Close();
                    }
                }
                else
                {
                    removeInfo = new List<RemoveItem>();
                    using (FileStream file = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write))
                    {
                        IFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(file, removeInfo);
                        file.Flush();
                        file.Close();
                    }
                }
                fileLock.ReleaseMutex();
            }
        }

        private object removeFromCache(CacheKey key)
        {
            if (this.checkInCache(key))
                return (this.Cache.Remove(key.ToString()) as CacheObject).Value;
            else return null;
        }

        private object removeFromCookies(CacheKey key)
        {
            this.openRemoveInfo();
            int idx = -1;
            RemoveItem itm = removeInfo.FirstOrDefault(ri => ri.RemoveKey == key.Key);
            if (itm == null) itm = new RemoveItem();
            else idx = removeInfo.IndexOf(itm);

            itm.RemoveFrom = WebStorageSouce.Cookies;
            itm.RemoveKey = key.Key;
            if (idx == -1)
                removeInfo.Add(itm);
            else
                removeInfo[idx] = itm;

            this.saveRemoveInfo();
            return this.getFromCookies<string>(key);
        }

        private object removeFromMemory(CacheKey key)
        {
            object value = null;
            if (this.checkInMemory(key))
                value = ((memoryCache[key.Key] as Hashtable)[key.Identifier] as CacheObject).Value;
            if (memoryCache.ContainsKey(key.Key))
                memoryCache.Remove(key.Key);

            return value;
        }

        private object removeFromSession(CacheKey key)
        {
            this.openRemoveInfo();
            RemoveItem itm = new RemoveItem();
            itm.RemoveFrom = WebStorageSouce.Session;
            itm.RemoveKey = key.Key;
            removeInfo.Add(itm);
            this.saveRemoveInfo();
            return this.getFromSession<object>(key);
        }

        private void saveRemoveInfo()
        {
            using (Mutex fileLock = new Mutex(true, "readInfo"))
            {
                fileLock.WaitOne();
                string filePath = HttpContext.Current.Server.MapPath("readInfo.bin");
                if (File.Exists(filePath))
                {
                    using (FileStream file = new FileStream(filePath, FileMode.Truncate, FileAccess.Write))
                    {
                        IFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(file, removeInfo);
                        file.Flush();
                        file.Close();
                    }
                }
                else
                {
                    using (FileStream file = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write))
                    {
                        IFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(file, removeInfo);
                        file.Flush();
                        file.Close();
                    }
                }
                fileLock.ReleaseMutex();
            }
        }
        #endregion

        #region Private Class

        [Serializable]
        private class RemoveItem
        {
            public WebStorageSouce RemoveFrom = WebStorageSouce.Cookies;
            public DateTime RemoveSetTime = DateTime.Now;
            public string RemoveKey = "";
        }

        #endregion
    }

}
